package caso2.mundo;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.Signature;
import java.security.SignedObject;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Date;
import java.util.HashMap;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import javax.security.auth.x500.X500Principal;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.x509.X509V3CertificateGenerator;

@SuppressWarnings("deprecation")
public class Codificador<E> {

	/**
	 * FILE ENCRYPTION CONSTANTS
	 */
	private static final String salt = "DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi";
	private static final String passphrase = "336534316162633634626337653766";
	private static final int iterations = 1536;
	private static final int keyLength = 256;
	private static final SecureRandom random = new SecureRandom();

	private static final String SEPARADOR = ";";
	private static final String COD = "UTF-8";

	private PublicKey publicServerKey;

	private byte[] privateSign;

	private byte[] symmetricSign;

	protected byte[] encodedFileBytes;
	protected byte[] fileDigest;
	protected File encodedFile;
	protected File originalFile;
	protected byte[] fileSign;
	protected byte[] fileCipherBytes;
	protected byte[] fileCommonBytes;
	protected byte[] fileDigestEncoded;

	private PublicKey publicClientKey;

	private PrivateKey privateClientKey;

	private X509Certificate serverCert;

	private X509Certificate clientCert;

	private Signature RSASignature;

	private MessageDigest SHADigest;

	private HashMap<String, E> fileData;

	public String getEncodedFileName() throws Exception {
		return encodedFile.getCanonicalPath();
	}

	public HashMap<String, E> getFileData() {
		return fileData;
	}

	public byte[] getPrivateClientSign() {
		return privateSign;
	}

	public byte[] getSymmetricSign() {
		return symmetricSign;
	}

	public PublicKey getClientPublicKey() {
		return publicClientKey;
	}

	public PublicKey getServerPublicKey() {
		return publicServerKey;
	}

	public Codificador() {

		/* Generate a DSA signature */

		try {

			KeyPairGenerator keyGen = KeyPairGenerator
					.getInstance("DSA", "SUN");

			SecureRandom random = SecureRandom.getInstance("SHA1PRNG", "SUN");
			keyGen.initialize(1024, random);

			KeyPair pair = keyGen.generateKeyPair();
			PrivateKey priv = pair.getPrivate();
			PublicKey pub = pair.getPublic();

			/* Create a Signature object and initialize it with the private key */

			Signature dsa = Signature.getInstance("SHA1withDSA", "SUN");

			dsa.initSign(priv);

			/*
			 * Now that all the data to be signed has been read in, generate a
			 * signature for it
			 */

			privateSign = dsa.sign();
			publicClientKey = pub;

			// VerSig verify = new VerSig(key, realSig);

		} catch (Exception e) {
			System.err.println("Caught exception " + e.getMessage());
		}
	}

	public Codificador(String[] newKeys, String query) {

		/* Generate a DSA signature */

		try {

			byte[] publicServer = destransformar(newKeys[0].split(SEPARADOR),
					query);// newKeys[0].split(SEPARADOR);
			byte[] privateClient = destransformar(newKeys[1].split(SEPARADOR),
					query);// newKeys[1].split(SEPARADOR);
			byte[] publicClient = destransformar(newKeys[2].split(SEPARADOR),
					query);// newKeys[2].split(SEPARADOR);

			KeyFactory kf = KeyFactory.getInstance("RSA");

			PKCS8EncodedKeySpec skeySpec = new PKCS8EncodedKeySpec(
					privateClient);
			PrivateKey privateKeyClient = kf.generatePrivate(skeySpec);

			X509EncodedKeySpec pkeySpec = new X509EncodedKeySpec(publicClient);
			PublicKey publicKeyClient = kf.generatePublic(pkeySpec);

			pkeySpec = new X509EncodedKeySpec(publicServer);
			PublicKey publicKeyServer = kf.generatePublic(pkeySpec);

			/* Create a Signature object and initialize it with the private key */

			RSASignature = Signature.getInstance("SHA1withRSA");
			SHADigest = MessageDigest.getInstance("SHA-1");

			RSASignature.initSign(privateKeyClient);

			/*
			 * Now that all the data to be signed has been read in, generate a
			 * signature for it
			 */
			privateSign = RSASignature.sign();
			publicClientKey = publicKeyClient;
			publicServerKey = publicKeyServer;

			RSASignature.initVerify(publicKeyClient);
			RSASignature.verify(privateSign);

			// VerSig verify = new VerSig(key, realSig);
			privateClientKey = privateKeyClient;
			createClientCertification(privateKeyClient);

		} catch (Exception e) {
			System.err.println("Caught exception " + e.toString());
		}
	}

	public static byte[] destransformar(String[] s, String ss) {
		byte[] b = new byte[s.length];
		for (int i = 0; i < b.length; i++) {
			try {
				b[i] = Byte.parseByte(s[i], 10);
			} catch (Exception e) {
				System.out.println(ss + SEPARADOR + e.getMessage());
			}
		}
		return b;
	}

	public static String transformar(byte[] b) {
		String s = null;
		try {
			s = new String("".getBytes(), COD);
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		for (int i = 0; i < b.length; i++) {
			if (i == b.length - 1)
				s += Byte.toString(b[i]);
			else
				s += Byte.toString(b[i]) + SEPARADOR;
		}
		return s;
	}

	public void createServerCertification(DataInputStream entradaDataEntidad) {
		try {
			int serverCertificateSize = entradaDataEntidad.readInt();
			byte[] certBytes = new byte[serverCertificateSize];

			for (int i = 0; i < serverCertificateSize; i++) {
				certBytes[i] = entradaDataEntidad.readByte();
			}
			ByteArrayInputStream certBytesArray = new ByteArrayInputStream(
					certBytes);
			// JDKX509CertificateFactory cf = new JDKX509CertificateFactory();
			CertificateFactory cf = CertificateFactory.getInstance("X.509");
			serverCert = (X509Certificate) cf
					.generateCertificate(certBytesArray);
			serverCert.verify(getServerPublicKey());
		} catch (Exception e) {
			System.err.println("Caught exception " + e.getMessage());
		}
	}

	private void createClientCertification(PrivateKey privateKeyClient) {
		try {
			// generate the certificate
			Security.addProvider(new BouncyCastleProvider());
			X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();

			certGen.setSerialNumber(BigInteger.valueOf(System
					.currentTimeMillis()));
			certGen.setIssuerDN(new X500Principal(
					"CN=Client Certificate (Recaudadora)"));
			certGen.setNotBefore(new Date(System.currentTimeMillis() - 50000));
			certGen.setNotAfter(new Date(System.currentTimeMillis() + 50000));
			certGen.setSubjectDN(new X500Principal("CN=Client Certificate"));
			certGen.setPublicKey(getClientPublicKey());

			// i get error here
			certGen.setSignatureAlgorithm("SHA256WithRSA");

			clientCert = certGen
					.generateX509Certificate(privateKeyClient, "BC");
		} catch (Exception e) {
			System.err.println("Caught exception " + e.getMessage());
		}
	}

	public void updateInformation(String newKey) {
		try {

			createSymmetricKeys(newKey, "VERIFIED");
			cipherFile(IEntidad.DATA_FILE);
			writeEncodedFileAndSetDigest();

		} catch (Exception e) {
			System.err.println("Caught exception " + e.getMessage());
		}

	}

	private byte[] configureCipher(String algorithm, int cipherMode,
			E privateSpec, byte[] publicObject) throws Exception {
		Cipher c = Cipher.getInstance(algorithm);
		c.init(cipherMode, (Key) privateSpec);

		if (algorithm == "AES") {
			int mod = publicObject.length % c.getBlockSize(); 
			int residuo = (mod == 0) ? 0 : c.getBlockSize() - mod; 
			byte[] paddedDoc = new byte[publicObject.length + residuo];
			// Pasar los datos del documento al nuevo arreglo de bytes para
			// permitir el correcto padding en el servidor
			for (int i = 0; i < publicObject.length; i++) {
				paddedDoc[i] = publicObject[i];
			}
			publicObject = paddedDoc;
		}

		return c.doFinal(publicObject);
	}

	@SuppressWarnings("unchecked")
	private void createSymmetricKeys(String newKey, String query)
			throws Exception {
		byte[] publicSymmetricClient = destransformar(newKey.split(SEPARADOR),
				query);

		byte[] cipherResult = configureCipher("RSA/ECB/NoPadding",
				Cipher.DECRYPT_MODE, (E) privateClientKey,
				publicSymmetricClient);

		byte[] symmetricKey = new byte[16];
		for (int i = 127; i >= 112; i--) {
			symmetricKey[127 - i] = cipherResult[i];
		}

		symmetricSign = symmetricKey;
	}

	@SuppressWarnings("unchecked")
	private void cipherFile(String file) throws Exception {
		SecretKeySpec specAES = new SecretKeySpec(symmetricSign, "AES");
		File records = new File(file);

		fileData = new HashMap<String, E>();

		originalFile = records;
		fileCommonBytes = IEntidad.getFileBytes(records);

		fileCipherBytes = configureCipher("AES", Cipher.ENCRYPT_MODE,
				(E) specAES, fileCommonBytes);

		fileSign = getSignForObject(fileCommonBytes);
		fileData.put("objectSign", (E) fileSign);
		fileData.put("file", (E) originalFile);
		fileData.put("cipherBytes", (E) fileCipherBytes);

	}

	@SuppressWarnings("unchecked")
	private void writeEncodedFileAndSetDigest() throws Exception {
		encodedFileBytes = Base64.encode(fileCipherBytes);

		String name = originalFile.getCanonicalPath() + ".encoded.csv";
		File encodF = new File(name);

		new FileOutputStream(encodF).write(encodedFileBytes);

		// SHADigest.update(fileCipherBytes);
		SHADigest.update(encodedFileBytes, 0, encodedFileBytes.length);

		byte[] digest = SHADigest.digest();
		// byte[] encodedDigestBytes = Base64.encode(digest);
		// //encodedDigest.getBytes();

		fileDigest = digest;
		fileDigestEncoded = Base64.encode(digest);
		encodedFile = encodF;

		fileData.put("fileDigest", (E) fileDigest);
		fileData.put("encodedFileBytes", (E) encodedFileBytes);
		fileData.put("encodedFile", (E) encodedFile);

	}

	private byte[] getSignForObject(byte[] object) throws Exception {
		RSASignature.initSign(privateClientKey);
		RSASignature.update(object);
		// SignedObject sObject = new SignedObject((Serializable) object,
		// privateClientKey, RSASignature);
		// sObject.verify(getClientPublicKey(), RSASignature);
		return RSASignature.sign();// sObject.getSignature();
	}

	public X509Certificate getClientCertification() {
		return clientCert;
	}

	public Certificate getServerCertificate() {
		return serverCert;
	}

	public void secureFile(File inFile) {
		try {

			Security.insertProviderAt(new BouncyCastleProvider(), 1);

			byte[] cipherFile = encrypt(inFile);

			FileOutputStream fos = new FileOutputStream(inFile.getAbsolutePath());
			fos.write(cipherFile);
			fos.close();


		} catch (Exception e) {
			System.out.println(e.getMessage());
		}
	}

	private static byte[] encrypt(File inFile) throws Exception{
		return generateCipher(inFile, Cipher.ENCRYPT_MODE);
	}

	public static byte[] decrypt(File cipherFile) throws Exception{
		return generateCipher(cipherFile, Cipher.DECRYPT_MODE);
	}

	private static byte[] generateCipher(File inFile, int mode) throws Exception{
		SecretKey key = generateKey(passphrase);
		Cipher cipher = Cipher.getInstance("AES/CTR/NOPADDING");
		
		byte[] ivBytes = new byte[cipher.getBlockSize()];
		random.nextBytes(ivBytes);

		cipher.init(mode, key, new IvParameterSpec(ivBytes), random);
		
		return cipher.doFinal(IEntidad.getFileBytes(inFile));
	}
	
	private static SecretKey generateKey(String passphrase) throws Exception {
		PBEKeySpec keySpec = new PBEKeySpec(passphrase.toCharArray(),salt.getBytes(), iterations, keyLength);
		SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWITHSHA256AND256BITAES-CBC-BC");
		return keyFactory.generateSecret(keySpec);
	}



}
